微分享回放 | 提高系统开发效率的“银弹”——X-series可视化大规模应用开发工具集
赫杰辉,携程框架研发部高级研发经理,负责携程DAL组件开发与推广。
在开发一线奋战多年的老兵,热爱中国传统文化和推广开源软件,希望用自己开发的工具为大家解决实际问题,愿为中国的开源事业贡献自己的绵薄之力。
*视频时长约1小时13分钟,请在WiFi环境下观看*
https://v.qq.com/txp/iframe/player.html?width=500&height=375&auto=0&vid=c0340vrpod1
子曰,知之为知之,不知为不知,是知也。
知道自己不知道也是一种知道,但作为开发人员,面对一个系统时,无论是开发新功能还是维护老系统,我们更多的是处在一种茫然无助,不知道如何下手,甚至不知道自己不知道的状态中。虽然系统开发的实践已经超过半个世纪了,在各个方面都取得了长足的进步,解决了很多难题,但我们在开发效率方面的提高明显跟不上系统规模的膨胀。虽然各种新想法,新方案虽然层出不穷,但始终都没成为大家心目中的那枚银弹。
本文试图从分析开发人员面临的困难与挑战入手,剖造根因,探索解决之道,最终通过提供工具来落实解决方案。也许没办法提供一枚完美的银弹,但钢弹铁弹也能打败怪物。
一、系统开发面临的挑战
对于一个拥有多年实践经验的开发人员来说,软件开发的本质其实是软件维护,因为任何一个系统从开发的第二天开始,就会面临一个理解的问题,摆在我们眼前的始终是如何理解昨天的系统和构建今天与明天的系统。
理解系统的途径无非是阅读文档和代码。但无论公司大小,只要对开发工作有所积累,都会发现通过文档和代码进行理解存在巨大的困难。虽然是老生常谈,但为了深入理解问题并针对性的提出解决办法,我们还是先花点时间聊聊。
1.1、文档迷宫
首先,人人都讨厌写文档。人的思维的速度是任何事物都无法匹敌的,嘴巴跟不上大脑,手更跟不上。因此我们往往发现找不到文档或即使找到,文档质量也很差。产品经理负责的需求文档可能好点,因为他们必须写,但开发人员负责的设计和说明文档其质量大家心里都有数。
其次,开发本质上是个翻译的过程。从最开始的想法到最终用户看到的实现,中间要经历多次的翻译过程:
需求 --> 设计 --> 代码
哪里有翻译,哪里就有误解。由于各个环节参与的人之间存在概念,惯用语等各方面的差异,存在误解是必然的,不误解是侥幸的。并且由于在各个环节之间的抽象程度不一样,在环节之间还存在细节的增强与丢失。这就是为什么文档往往缺乏关键的实现细节。
常见的情况是很多需求确认的内容会在口头,电话或邮件中表述,但没有反映到文档里面,虽然最初参与沟通和系统实现的人按照这种需求做了,但后继维护的人就无法找到原始的需求来源。
最后,很隐蔽,但很关键的一点是文档之间的无关联性。需求文档与设计文档,设计文档与代码本质是割裂的,没有关联的。任意文档的改动不会引起其他文档的自动同步。
这事实上决定了文档是不可信的。即使找到一些文档,这些文档也都很少反映最新的需求和系统现状。也许最初的文档写的很工整,与实际系统大致吻合,但几个版本以后,文档一般都会变得要么支离破碎,要么结构混乱,最惨的是根本就忘了更新。
这是现实,讨论对错没有什么意义。最终悲催的开发人员只能依赖源码,从源码反推需求。也就是那句著名的“Talk is cheap, show me the code”
1.2、源码泥潭
通过看代码理解系统是没有办法的办法。小公司,小模块里几千,上万行代码的系统我们也许可以这么做,但面对一个百万,千万代码行级别的系统,我们本质上无法通过阅读代码来进行理解的。
经常会听到某某技术公司的CEO/CTO说其会看代码,以表示自己多么的hands-on。我们做个简单的算术,假定一个人1天可以看完1万行代码,100万代码需要近3个月时间,很难想象一个高层人士3个月什么事情也不做,只是看代码,并且代码每天都在变化。靠对细节的观察反推宏观系统,这种思路是错误的。
系统的动态几乎不可能通过人工的静态分析推测出来。看代码只能检查一些很表面的东西,几乎所有的代码检查都以对格式或命名之类的细节争论收场。当然不能说看代码完全没用,在一个很小的团队内部,大家都对系统很熟悉的情况下,这样做可能是有效的;但放到cross team的情况下,代码review最终会变为形式或者一个social activity而已。
当然我们最终还是得看源码的。谈到看代码,大家心里想的一定是能不看就不看,因为大多数的源码真的是惨不忍睹。对于任何一个有点积累的公司来说,到处都是超长的类,超长的方法;超大,超长,超宽的嵌套条件分支;硬编码的对象组装逻辑等等,更别谈各种新兴的AOP和字节码技术埋伏在各种和你眼前的代码完全无关的想不到的角落。这样的源码要么完全让人无法读懂,要么即使看完了也不敢说自己真的明白怎么回事。
系统开发并不仅仅意味着写代码,问题的规模不同,解决的手段也不同。不幸的是我们开发任何规模的系统用的办法都似乎是同一个原始的手段--写代码。对于一个小系统,直接上代码也许没问题,但对于大多数工业级别的系统,由于规模不同,这么做只能是低效的甚至是失败的。
从整个系统的角度来看,代码仅仅是最底层实现的细节。代码可以完成很多事情,但不意味着代码是解决所有开发问题最有效的手段。对于写给人类看的代码来说,代码只适合描述简短的顺序逻辑,分支和嵌套结构。
通过看代码来理解一个系统是最低效的方式,好比一个人试图通过双脚走遍一个森林的每个角落来理解整个森林一样,本质上是不可能完成的任务。
二、解决思路
问题意味着机会,思路决定着出路。如果承认提高系统开发效率的关键在于如何快速高效的理解系统,那么我们的解决问题的方向和判断标准就很明确。
先排除不正确的思路,我认为有如下几个:
堆砌流程。流程越多越痛苦。这个无需多说。真正该做的事是现有流程的简化与自动化。流程本质上跟提高理解系统和构建系统的效率无关。系统开发的主要工作是理解并构建一个可以运行的系统,而不是构建一套工作流程。参与到流程中的时间必然会挤占掉实际开发的有效时间。
开发更多的管理工具。大多数工具做的是和开发无关的周边管理工作,例如编译,持续集成,源码管理,发布等等。同流程一样,管理工具越多,开发工作越艰难。经常发现为了统一目前过多的解决方案,大家头脑一热就又做了一个类似的。往往很多系统之间对关键数据都对不上号。这个道理跟航海时用一个钟表还是两个钟表的问题一致。
贸然引进与目前类似的新语言/框架/标准。公司级别的开发实践和个人的开发实践是两个完全不同概念。个人如同武林高手,多学习新东西是自我成长的必要,艺多不压身,最好成为全才;公司如同军队作战,讲究的是快速培训,快速上手,团队合作,同一标准,简洁高效,对人的要求是中众就行。引入过多的语言/框架/标准会导致对人员要求不必要的提高。
因此对公司开发而言必须要对引入新东西保持警惕。如果没有新东西,不符合之前提到的标准,对开发效率没有很大的提高,又或者仅仅只是重复解决已经被现有方法解决了的问题,这种引入就是失败的,只会增大整个系统的复杂度,增加理解的难度,进一步拉低效率。
要构建一个可以快速高效理解的系统,正确的思路主要是为系统构建提供一种简单易懂,无需翻译的方式。其次由于不同领域的模型千差万别,无法通过一个统一的模型来描述,针对不同的问题领域,我们需要合适的模型和专用的建模工具。因此正确的方案需要做到以下几个方面:
2.1、图形化编辑
俗话说百闻不如一见。由于人天生对图形比对文字理解来的更快,更自然。因此一个高效的工具必须首先是可视化的。从系统顶层模型就开始通过可视化工具来构建,直到不得不借助代码来实现的细节层面之上为止。同时由于某些问题领域可能具有相当的复杂度,往往一张图不能完全表达整个系统,所以必要时需要支持子图,要做到主图与子图之间的相互关联。通过这种方式做到从抽象到具体的层级递进,让人拥有看系统的“鹰眼”。既可以查看高层的结构,又可以快速定位到某个特定的抽象层次。
2.2、基于模型而不是代码
很多代码其实描述的是业务模型或者数据模型,虽然能运行,但是是无法理解或者实现上非常笨拙。正确的做法是将业务模型和数据模型从代码里面解放出来,模型就用能最直接表示模型特点的方式来描述。实践中有一种做法是试图通过代码生成来完成模型到实现的关联,但大部分场景会存在上面提到的细节丢失问题,导致无法有效做到双向同步。要做到模型的有效性,最好的做法是直接使用模型而不是代码生成,并且在开发和运行时是同一个。
图1
通过以上两点把系统开发的方法从简单的堆砌代码提升到基于可视化模型的层次。方法不同,层次不同,描述系统的的广度和难易程度就不同,理解系统的效率将大大提高。
那么情形值得我们抽象成特定的模型?根据实际工作中的痛点,行为,决策和状态这几个模型值得抽象出来。
2.2.1行为模型
站在用户和开发的角度,一个系统是由其提供的服务来定义。系统有哪些服务,完成一个服务需要执行那些步骤,按照什么路径执行,是理解系统的关键。行为模型可以通过流程图来可视化表达。流程图可以清晰的描述一个服务是如何一步步的完成。从代码的角度而言,引入流程图可以消灭大部分的粘合,判断代码。有利于把一个单体系统拆分为易于理解的子系统,并进一步拆分为具体的步骤。
也许有人会问为什么不用对象图或时序图,原因如下:
1)对象图显示实体间的关系而不是动作如何完成,对系统动态理解没帮助
2)时序图仅能描述特定执行路径,而无法直观表述分支/循环,对系统动态的描述不完整,也不友好
X-Series工具集里面的Xross Unit就是利用流程图构造系统的工具。
2.2.2决策模型
一个决定受哪些因素影响,每个因素的可能取值有哪些,按照什么顺序考虑因素获得决策。决策模型可以通过决策树来可视化表达。这种方式可以直观的表达复杂逻辑判断的分支和判断标准。可以用来取代复杂嵌套的if/else。
X-Series工具集里面的Xross Decision就是利用决策树构造决策模型的工具。
2.2.3状态模型
一个实体具有哪些状态,状态之间如何转移。状态模型可以通过状态机来可视化表达。可以代替复杂的hard-code的状态判断和动作触发。
X-Series工具集里面的Xross State就是利用状态机描述模型状态的工具。
三、X-series简介
工欲善其事 必先利其器。X-series是一套轻量级的,易于学习,易于使用,易于测试,易于交流的框架。目的是解决大规模软件开发中沟通不畅,文档不新,分工不当,进度不明等难题。它包括3个组件:
1、XrossUnit:用流程图描述服务如何按步骤完成
2、XrossDecision:用决策树为复杂决策建模
3、XrossState:用状态机管理业务状态变迁
这三个组件互相之间没有任何耦合,根据实际需要,在一个系统里面即可以单独使用,也可以配合使用。这些组件对运行的平台也没有要求,即可以运行在容器里面,也可以单独运行在应用程序里面。
另外还有一个正在开发中的基于SEDA的微服务框架XEDA,属于运行平台级别。整体的范围的关系如下:
图2
四、Xcorss unit
4.1、简介
XrossUnit是一个基于流程图的灵活的系统构建器,又称xUnit。用户在Eclipse里面通过Xross Unit编辑器创建系统服务,编辑服务流程。运行时通过Xross Unit工厂类来获得并执行定义的流程。
图3
XrossUnit支持行为组件和结构组件。行为组件又称为单元,是Xross Unit名字中Unit的来源。行为组件通过接口定义规范对数据处理的方式,是构成模型的基本元素。结构组件提供预定义的结构,方便在行为组件之上用更大的粒度构造流程结构。结构组件可以指定自己表现为那种行为。
XrossUnit的范围包括流程图模型和其中的配置信息,不包括组件内部的代码实现。组件内部代码需要通过对行为组件的接口实现来完成。
XrossUnit关联了模型与代码,Xross Unit依据模型规划的路径自动调度各个行为组件。想要看任意组件的代码仅仅需要双击即可进入到实现类的内部。
行为组件通过接口定义,包括:
1、Processor。仅对输入的Context进行处理,没有返回值
2、Converter。将输入Context转换为输出Context
3、Validator。对Context进行true/false判断
4、Locator。对Context进行定位分类判断
行为组件即可以单独使用,也可以相互组合为更大的结构。
结构组件为按照一定结构预定义的一系列行为组件,包括:
1、Chain。顺序执行一系列单元
2、If-else。根据Validator的判断,决定执行两个分支中的哪一个
3、Branch。根据Locator的判断,决定执行多个分支中的哪一个
4、While循环。根据Validator的判断,决定是否执行包含的单元,并在执行结束后回到Validator再次判断
5、Dowhile循环。在执行完包含的单元后根据Validator判断决定是否再次执行
6、Decorator。对任意单元进行修饰,在单元执行前后做额外动作
7、Adapter。对任意单元做行为上的转变,可以用于复用现成组件
XrossUnit编辑器提供了直观的编辑方法,可以将现有已有单元或结构通过的简单的对象组合来生成新的结构。例如需要在一个单元原来的处理流程上面需要添加一个新的流程判断,我们可以在界面上面选择Validator再单击需要添加分支判断的单元或结构就会在原有基础上增加一个分支结构。编辑器本身支持undo/redo功能。通过这种方式可以快速的从无到有的构建一个复杂的系统,同时保证系统易于理解。
XrossUnit编辑器提供自动排版,用户仅仅需要在图上添加修改单元,编辑器会自动根据组件间的关系对布局做调整,无需人工干预。无论谁来生成,模型都是一致的,始终保持图的清晰,准确与美观。
XrossUnit还支持配置,可以在应用或构建单元层次上面配置参数,方便在不同场景下复用同一个模型。
4.2、Xross Unit使用方式
1)构建系统蓝图。可以一个人或大家一起边讨论,边通过编辑器画出要开发的系统的流程图。每个组件都会有缺省的桩实现。因此图画好了,这个系统就可以马上运行。
图4
2)实现组件单元。针对流程图中的每个行为组件实现相应的接口,提供实际的处理能力。
图5
3)关联蓝图和代码。在编辑器里面双击行为组件,指定实现类的名字。
图6
4)生成实例并运行。
4.3、Xross Unit实际示例
我们看一个实际例子来说明其优势,这是一个实际使用了Xross Unit构建系统的一部分例子。
系统顶层主流程
图8
主流程中请求签名验证对应的子图
图9
主流程中中业务处理对应的子图
图10
主流程中返回值处理对应的子图
图11
具体实现的代码不方便提供,但可以看到流程图可以有效的描述和分解系统。这难道不是你们渴望已久的工具吗?
4.4、Xross Unit的优势
趁手的工具是原则保证的利器,通过上面的例子我们可以看到使用Xross Unit:
首先可以做到快速组建系统:
1、自顶向下分解,组件化设计,流水线式开发
2、最优化设计复用
3、在流程模型与代码之间快速切换。无需脱离开发环境
其次可以自然做到系统开发提倡的高内聚,低耦合原则。通过没有工具的保证,这些原则只能沦为口号:
1. 通过名字描述功能
2. 通过配置调整行为
3. 通过Context限定数据
4. 每个unit仅仅完成明确描述的功能,有效控制代码复杂度
最后这种构造系统的方式非常易于单元化测试:
1. 单接口设计,无选择,无歧义的实现
2. 通过构造Context,轻松模拟测试数据
3. 可以在组件级别快速提供mock object
4.5、Xross Unit不是什么
不是又一个Spring
Spring是从整体如何由局部构成的观点构建系统。Spring的做法其实是完成类图/对象图的模型化,如前所述,这种图无法描述系统动态。xUnit是从请求如何被处理的行为观点构建系统。
不是工作流
工作流处理多角色在多请求之间的任务/路径管理,是更大范畴的可视化。xUnit细化的是单个请求级别的响应路径/处理单元
不是一个可视化的编程语言
可视化的编程语言的做法是解释和生产代码。xUnit将粘合代码抽取为模型,在业务层组装行为和结构单元,
xUnit的系统定位如下图
图12
4.6、Xross Unit常见问题
1)为什么使用单元来完成代码也能做的事情?
因为问题的大小决定手段的选择,想象下面工作的复杂度
i. “Hello World”
ii. 一个Web Service
iii. 一个小的Web App
iv. 一个淘宝,ebay,ctrip规模的网站
简单的系统可以直接用代码实现,复杂的系统无法这么做。单打独斗和大规模开发是完全不同的实践。大规模开发的关键是系统理解,不但要最初开发的人理解,后面维护的人和测试,产品等相关人员也都要理解。随着开发规模的不断膨胀,最初方法的效果一定会发生变化,手段必然要求变化。由于人总是习惯于当前的做法,这种变化容易被人忽略,有些人可能意识到了问题所在,但没兴趣改变。还有人希望改变,但或者由于工作负担实在太大,没时间停下来思考,或者不具备相关的技术,无法做出改进。
目前流行的微服务的产生本质上也是回应这种规模带来的变化,但没有可视化的支持,微服务除了把一个请求放大到多个请求外,一样没出路。
2)为什么不用现有的命令框架
常见的命令框架如Servlet,JEE里面的SessionBean, Entity Bean, Message Bean等等,描述的粒度仅限于系统入口服务。缺乏入口服务级别下的内部细节表示。我们可以观察到尽管有大量的小的仅仅只有一页代码的command,但是还是会有少量但是非常重要的command非常的复杂,这符合普遍的80/20原则。而这是系统理解最关键的一部分。
五、Xross Decision
5.1、简介
XrossDecision又称为xDecision,是一个图形化的决策树编辑器与运行时工具。以所见即所得的方式构造复杂逻辑判断的过程。同时还可以依据模型生成单元测试的验证代码。xDecision可以用于替代传统的复杂的if/else判断,能极大的简化代码并让业务逻辑易于理解和维护。
图13
5.2、Xross Decision概念
1. 决策树。决策树是由判断节点和其连线组成的模型。每个判断节点可以包含决策或进一步判断的因素
2. 因素。一个决策可以由多个因素所决定。一个因素是包括多个可能取值的变量。
3. 决策。决策就是用户定义的包含特定含义的常量
5.3、Xross Decision构建
1. 定义决策的考虑因素。一个决策可以由多个因素所决定。一个因素是包括多个可能取值的变量。
2. 定义决策。决策就是用户定义的包含特定含义的常量
3. 添加判断节点。节点可以包含当前节点的决策和进一步判断所依据的因素
4. 连接节点。节点之间可以相连,连线需要赋予父节点因素的某个取值
可以在编辑器的空白出右键点击出菜单完成因素和决策的定义。通过边栏菜单添加节点和连线。
图14
5.4、Xross Decision测试与使用
编辑完成后可以通过生成单元测试的方式来验证模式是否正确,同时单元测试也演示了实际使用如何进行。
图15
单元测试是标准的Junit测试代码,覆盖了模型中每一条可以到达决策的路径,可以直接运行
图16
六、Xross State
XrossState是可视化创建状态机的编辑器,又称为xState。状态机的用处极其广泛,可以说是很多系统的核心。与xUnit类似,xState可以结合模型和代码。即可以创建仅包含状态和变迁的状态机,也可以提供状态变迁时的触发器。
图17
状态转移触发器
1. EntryAction。进入下一个状态时触发
2. ExitAction。离开当前状态时触发
3. TransitionAction。在状态迁移时触发
状态转移校验
1. TransitionGuard。判断状态变迁是否合法
图18
XrossState的使用与其他两个类似,这里就不细说。下面是运行时的示例代码
图19
一个用户的实际案例:
图20
七、X-Series使用小结
无论是xUnit,xDecision还是xState,其使用方式都是:
1. 创建模型
2. 修改模型元素属性
3. 实现对应接口。这一步不是必需的,只有xUnit需要,xState只有当需要定义触发器时才需要,xDecision不需要
4. 运行模型。通过工厂类加载模型文件,获得对应模型,创建数据,处理数据
图21
八、Xeda预览
随着微服务的兴起,微服务的使用在服务层面对系统理解同样带来了挑战。微服务的使用对系统的拆分,重构,运维,监控和发布有很高的要求,其复杂度大大超过传统系统架构。
图22
Xeda是一个正在开发中的可视化微服务编辑器,可以提供一个基于SEDA模型的微服务实现。其目的是:
1. 提供微服务可视化编排
2. 提供微服务运行时框架
3. 提供微服务自动部署
4. 提供微服务系统层面的监控
图23
九、X-Series资源
1. 代码和示例
a)
b)
c)
2. 安装包
a)
X-Series的编辑器运行于Eclipse里面。目前没有idea的实现。支持的语言为Java。我的同事王晔楠提供了C#的运行时实现,C#的用户可以在Eclipse里面构建模型,在C#环境中运行:
1. Xrossunit C# runtime
a)
2. XrossState C# runtime
a)
3. XrossDecision C# runtime
a)
十、结束语
10.1、X-Series开发简史
X-Series是我独立开发的一套为了提高开发效率,减轻开发人员工作负担,提高系统架构质量的工具。是我多年的心血。
最初是在2001年的时候在一个在线拍卖项目中有了开发xUnit的想法,但当时想法还没成熟,也没有可视化编辑器的开发能力,暂时作罢。后来在2006年东南融通做架构师,为北京农行的清算项目做架构的时候,构建了一套基本接口并基于Spring做模型集成,同事试用下来反馈很好,可以大幅降低系统的理解难度,减低维护难度,提高开发速度。这次尝试验证了通过流程模型构建系统的可行性,让我坚定了信心,开始深入思考如何完善模型并积蓄必要的开发能力。后来加入ebay的时候,在无数个加班的后半夜,一个人坐在办公室里面面对天量的陌生代码时,我无数次的渴望有类似xUnit这样的工具来帮我快速理解系统。
2007年的时候在ebay做搜索页面的时候为了消除复杂到变态的条件分支语句,做了最初的xDecision运行时框架。当时的实现方式是用接口定义决策树模型,还没有可视化编辑器。xDecision在ebay的使用获得了成功,并给大家做了分享,引起了热烈的讨论。
2012年,通过之前的项目获得了Eclipse插件开发能力,才完成了xUnit和xDecision的可视化编辑器原型并参加了ebay的skunkwork比赛并入围最后的全球展示。后来继续完善这两个系统并开发了xState,直到今天给大家演示的系统。其中的艰辛一言难尽。
10.2、大规模系统的建议
大规模系统开发的难点是系统理解,我的看法是在语言层面打转是没有出路的,无论哪种语言都无法带来数量级上的代码简化和理解效率提升,语言这种基于文字,顺序描述问题的方式天生决定了它不适合在模型层面可视化描述系统。现代系统开发的瓶颈是人,而不是机器,流程或工具。大规模开发必须通过合适的工具来提高人的效率才能获得突破。可视化技术的表达能力相对语言是维度上的突破,可视化技术已经在很多领域都有广泛的应用,例如大规模集成电路设计,建筑设计等等,是已经验证过的成熟解决方案,我认为这是正确的方向。
方向和眼光永远比速度重要。一个人不可能精通所有的语言和工具,请谨慎选择合适的工具并确保充分利用工具的潜力。不要什么都来一点,但每个都不精,每个都只用一小部分功能。那样只会花费大量的时间但最终却导致系统复杂到无法理解,无法交接和维护。事物的发展往往在发展中期会变得特别复杂,这正是目前大规模系统开发的状态。但成熟后又会变得简洁直观,比如使用X-Series轻松,可控的开发。
现在的系统越来越复杂,使用X-Series可以标准化绝大多数系统开发的流程。这种做法可以作为系统开发原则成为架构文档的组成部分:
首先用xUnit构造通用的系统顶层请求处理流程,并罗列主要服务;其次用xUnit细化各个服务的流程;对于涉及到复杂条件判断的地方使用xDecision;对明确的状态转换使用xState。
需求变更引起的修改要么只涉及到具体实现,要来只涉及到模型,这种范围的明确划分减少了不必要的干扰。模型的变更会直接反映到系统的动态表现。如果改动只涉及到模型,代码完全不必修改。
快速高效的开发大规模系统的能力是现今公司竞争的巨大优势,快打慢,会打不会是一般的规律。具有这种能力的公司将立于不败之地。或早或晚,未来的开发都会使用X-Series或类似工具。不如此,人类无法构建更宏伟的系统。新的事物最开始往往不被理解,普遍使用后却觉得本来应该如此。我对X-Series也有这样的期待。
请开始使用X-Series。
啊,你看到这里啦~
携程技术中心目前开设了架构/移动/大数据/前端/运维5个微信群,如想进群交流,请加携程技术中心小助手个人微信号ctirp_tech,标注相关领域(如大数据),小助手会拉你入群哦。
往期回顾:
喜欢我们的会点赞,爱我们的会分享~